/*    Copyright 2010 Tobias Marschall
 *
 *    This file is part of MoSDi.
 *
 *    MoSDi is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    MoSDi is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with MoSDi.  If not, see <http://www.gnu.org/licenses/>.
 */

package mosdi.subcommands;

import java.util.List;
import java.util.StringTokenizer;

import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import mosdi.fa.Alphabet;
import mosdi.util.Log;
import mosdi.util.SubcommandApplication;

public abstract class Subcommand {

	protected OptionSet optionSet;
	protected List<String> positionalArguments;
	
	protected Subcommand() {
		optionSet = null;
	}
	
	/** Runs this subcommand.
	 * 
	 * @param args Command-line parameters.
	 */
	public abstract int run(String[] args);

	/** Returns usage text to be shown to user. Allowed to contain newline characters. */
	public String usage() {
		return SubcommandApplication.getInstance().getApplicationName()+" [global-options] "+name();
	}

	void printUsageAndQuit() {
		Log.errorln(usage());
		System.exit(1);
	}

	/** Returns a short description of the command to be shown to the user. Should not
	 *  contain newline characters. */
	public abstract String description();

	/** Returns the name to be given at command line to use this command. */
	public abstract String name();

	protected void parseOptions(String[] args, int nonOptionArguments, String optionString) {
		OptionParser optionParser = new OptionParser(optionString);
		try {
			optionSet = optionParser.parse(args);
		} catch (OptionException e) {
			Log.errorln("Error parsing command line: "+e.getMessage());
			System.exit(1);
		}
		if (optionSet.nonOptionArguments().size()!=nonOptionArguments) {
			printUsageAndQuit();
		}
		positionalArguments = optionSet.nonOptionArguments();
	}
	
	protected int getIntOption(String option, int defaultValue) {
		if (optionSet.has(option)) {
			try {
				return Integer.parseInt((String)optionSet.valueOf(option)); 
			} catch (NumberFormatException e) {
				Log.errorln("Error: Illegal parameter to option -"+option+": Integer expected.");
				System.exit(1);
			}
		}
		return defaultValue;
	}

	protected double getDoubleOption(String option, double defaultValue) {
		if (optionSet.has(option)) {
			try {
				return Double.parseDouble((String)optionSet.valueOf(option)); 
			} catch (NumberFormatException e) {
				Log.errorln("Error: Illegal parameter to option -"+option+": Floating point value expected.");
				System.exit(1);
			}
		}
		return defaultValue;
	}

	protected double getRangedDoubleOption(String option, double lowerBound, double upperBound, double defaultValue) {
		if (optionSet.has(option)) {
			try {
				double value = Double.parseDouble((String)optionSet.valueOf(option));
				if ((value<lowerBound) || (value>upperBound)) {
					Log.errorln("Error: parameter to option -"+option+" must be between "+lowerBound+" and "+upperBound+".");
					System.exit(1);
				}
				return value;
			} catch (NumberFormatException e) {
				Log.errorln("Error: Illegal parameter to option -"+option+": Floating point value expected.");
				System.exit(1);
			}
		}
		return defaultValue;
	}

	protected long getNonNegativeLongOption(String option, int defaultValue) {
		if (optionSet.has(option)) {
			try {
				long value = Long.parseLong((String)optionSet.valueOf(option));
				if (value<0) {
					Log.errorln("Error: Parameter to option -"+option+" must be >=0.");
					System.exit(1);
				}
				return value;
			} catch (NumberFormatException e) {
				Log.errorln("Error: Illegal parameter to option -"+option+". Integer expected.");
				System.exit(1);
			}
		}
		return defaultValue;
	}

	
	protected int getNonNegativeIntOption(String option, int defaultValue) {
		if (optionSet.has(option)) {
			try {
				int value = Integer.parseInt((String)optionSet.valueOf(option));
				if (value<0) {
					Log.errorln("Error: Parameter to option -"+option+" must be >=0.");
					System.exit(1);
				}
				return value;
			} catch (NumberFormatException e) {
				Log.errorln("Error: Illegal parameter to option -"+option+". Integer expected.");
				System.exit(1);
			}
		}
		return defaultValue;
	}

	protected int getPositiveIntOption(String option, int defaultValue) {
		if (optionSet.has(option)) {
			try {
				int value = Integer.parseInt((String)optionSet.valueOf(option));
				if (value<=0) {
					Log.errorln("Error: Parameter to option -"+option+" must be >0.");
					System.exit(1);
				}
				return value;
			} catch (NumberFormatException e) {
				Log.errorln("Error: Illegal parameter to option -"+option+". Integer expected.");
				System.exit(1);
			}
		}
		return defaultValue;
	}

	protected String getStringOption(String option, String defaultValue) {
		return optionSet.has(option)?(String)optionSet.valueOf(option):defaultValue;
	}
	
	protected int[] getStringOption(String option, Alphabet alphabet, int[] defaultValue) {
		return optionSet.has(option)?alphabet.buildIndexArray((String)optionSet.valueOf(option)):null;
	}
	
	protected int getIntArgument(int number) {
		try {
			return Integer.parseInt(positionalArguments.get(number));
		} catch (NumberFormatException e) {
			Log.errorln("Error: Illegal "+(number+1)+". argument (\""+positionalArguments.get(number)+"\"): integer expected.");
			System.exit(1);			
		}
		return -1;
	}
	
	protected String getStringArgument(int number) {
		return positionalArguments.get(number);
	}
	
	protected <T extends Enum<?>> T getEnumArgument(int number, T[] enumValues, String ... names) {
		if (enumValues.length!=names.length) throw new IllegalArgumentException("Array length mismatch");
		T result = null;
		for (int i=0; i<names.length; ++i) {
			if (names[i].equals(positionalArguments.get(number))) result = enumValues[i];
		}
		if (result==null) {
			StringBuffer sb = new StringBuffer();
			sb.append("Error: Illegal "+(number+1)+". argument (\""+positionalArguments.get(number)+"\"): must be one of ");
			boolean first = true;
			for (String name : names) {
				if (!first) sb.append(", ");
				sb.append('"'+name+'"');
				first = false;
			}
			Log.errorln(sb.toString());
			System.exit(1);
		}
		return result;
	}

	protected boolean getBooleanOption(String option, boolean defaultValue) {
		return defaultValue ^ optionSet.has(option);
	}
	
	protected int[] getIntTupleOption(String option, int count, int[] defaultValue) {
		return getRangedIntTupleOption(option, count, Integer.MIN_VALUE, Integer.MAX_VALUE, defaultValue);
	}
	
	protected int[] getRangedIntTupleOption(String option, int count, int lowerBound, int upperBound, int[] defaultValue) {
		if (optionSet.has(option)) {
			int[] result = new int[count];
			StringTokenizer st = new StringTokenizer((String)optionSet.valueOf(option),",",false);
			int i = 0;
			while (st.hasMoreTokens()) {
				i += 1;
				if (i>count) break;
				String token = st.nextToken();
				try {
					int k = Integer.parseInt(token);
					if ((k<lowerBound) || (k>upperBound)) {
						Log.errorln("Error: parameter to option -"+option+" must be a comma-separated list of values between "+lowerBound+" and "+upperBound+".");
						System.exit(1);
					}
					result[i-1] = k;
				} catch (NumberFormatException e) {
					Log.errorln("Error: Illegal parameter to option -"+option+": \""+token+"\" is not a valid integer.");
					System.exit(1);			
				}
			}
			if (i!=count) {
				Log.errorln("Error: Invalid argument to option -"+option+": expected comma-separated list with "+count+" elements.");
				System.exit(1);
			}
			return result;
		}
		return defaultValue;
	}
	
	protected <T extends Enum<?>> T getEnumOption(String option, T defaultValue, T[] enumValues, String ... names) {
		if (enumValues.length!=names.length) throw new IllegalArgumentException("Array length mismatch");
		if (optionSet.has(option)) {
			T result = null;
			String value = (String)optionSet.valueOf(option);
			for (int i=0; i<names.length; ++i) {
				if (names[i].equals(value)) result = enumValues[i];
			}
			if (result==null) {
				StringBuffer sb = new StringBuffer();
				sb.append("Error: Invalid argument to option -"+option+": must be one of ");
				boolean first = true;
				for (String name : names) {
					if (!first) sb.append(", ");
					sb.append('"'+name+'"');
					first = false;
				}
				Log.errorln(sb.toString());
				System.exit(1);
			}
			return result;
		}
		return defaultValue;
	}

	protected void exclusiveOptions(String ... options) {
		for (int i=0; i<options.length-1; ++i) {
			for (int j=i+1; j<options.length; ++j) {
				if (optionSet.has(options[i]) && optionSet.has(options[j])) {
					Log.errorln("Error: Options -"+options[i]+" and -"+options[j]+" are mutually exclusive.");
					System.exit(1);
				}
			}
		}
	}

	/** If option is present then impliedOptions must also be present. */
	protected void impliedOptions(String option, String ... impliedOptions) {
		if (!optionSet.has(option)) return;
		for (String o : impliedOptions) {
			if (!optionSet.has(o)) {
				Log.errorln("Error: When option -"+option+" is given, option -"+o+" must also be given.");
				System.exit(1);
			}
		}
	}

}
